package com.mintyfresh.tns.util;

import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Date;
import java.util.Collections;

/**
 * This is a message class which can do EVERYTHING.
 *
 * It stores data in a Map so storage is unlimited, messages should
 * still serialize quickly, since only interned strings can be stored
 * as keys, and back references will be common.
 *
 * Conventience methods are provided for retrieving and entering
 * evey kind of primative, plus Object and String.
 * Also, a toString() method works for all messages w/out
 * tediously reprogramming output for each field.
 *
 * TODO: Implement a more efficient serialization mechanism.
 * I've got a technique planned which will cut the
 * number of objects by half (guess how?).
 * Bonus points to anyone who implements it before me.
 *
 * @author Adam Freidin
 */

public class Message implements java.io.Serializable, Cloneable {
	/**
	 * @serial include
	 */
	private final Map map; 

	public Message(String type) {
		map = new HashMap();
		set("#type", type);
	}

	public Message() {
		this("#UNTYPED");
	}

	public Message(Message message) {
		this(message.map);
	}

	public Message(Map map) {
		if(map == null) {
			this.map = new HashMap();
		} else {
			this.map = new HashMap(map);
		}
		set("?#type", "#UNTYPED");
	}

	public synchronized Message set(String name) {
		int colon = name.indexOf(':');
		if(colon != -1) {
			String key = name.substring(0, colon);
			String value = name.substring(colon + 1);
			return set(key, value);
		} else {
			map.put(name, null);
			return this;
		}
	}

	public synchronized Message unset(String name) {
		map.remove(name);
		return this;
	}

	public synchronized Message set(String name, Object data) {
		if(name.startsWith("?")) {
			name = name.substring(1);
			if(has(name))
				return this;
		}
		map.put(name.intern(), data);
		return this;
	}

	public synchronized Message set(String name, boolean data) {
		return set(name, new Boolean(data));
	}

	public synchronized Message set(String name, byte data) {
		return set(name, new Byte(data));
	}

	public synchronized Message set(String name, short data) {
		return set(name, new Short(data));
	}

	public synchronized Message set(String name, char data) {
		return set(name, new Character(data));
	}

	public synchronized Message set(String name, int data) {
		return set(name, new Integer(data));
	}

	public synchronized Message set(String name, long data) {
		return set(name, new Long(data));
	}

	public synchronized Message set(String name, float data) {
		return set(name, new Float(data));
	}

	public synchronized Message set(String name, double data) {
		return set(name, new Double(data));
	}

	/** Won't throw exceptions, may return null for multiple reasons. */
	public synchronized Object check(String name) {
		return map.get(name);
	}

	public synchronized Object get(String name) {
		if(has(name)) 
			return map.get(name);
		else
			throw new UnsupportedOperationException(
					"the field: " + name + " was not found in " + 
					"this message\n" + toString());
	}

	private synchronized String getTS(String name) {
		return get(name).toString();
	}

	public synchronized String getz(String name) {
		try {
			return (String)get(name);
		} catch (ClassCastException ex) {
			throw new UnsupportedOperationException(
					"the field: " + name + " is a " +
					get(name).getClass() + " not a String in " +
					"this message\n" + toString());
		}
	}

	public synchronized boolean getp(String name) {
		try {
			if(has(name)) {
				return get(name) == null 
					? false 
					: ((Boolean)get(name)).booleanValue();
			} else {
				throw new UnsupportedOperationException(
						"the field: " + name + " was not found in " + 
						"this message\n" + toString());
			}
		} catch (ClassCastException ex) {
			if(getTS(name).equalsIgnoreCase("true"))
				return true;
			else if(getTS(name).equalsIgnoreCase("false"))
				return false;
			throw new UnsupportedOperationException(
					"the field: " + name + " is a " +
					get(name).getClass() + " not a Boolean in " +
					"this message\n" + toString());
		}
	}

	public synchronized byte getb(String name) {
		try {
			return ((Byte)get(name)).byteValue();
		} catch (ClassCastException ex) {
			try {
				return Byte.parseByte(getTS(name));
			} catch (NumberFormatException e) { }

			throw new UnsupportedOperationException(
					"the field: " + name + " is a " +
					get(name).getClass() + " not a Byte in " +
					"this message\n" + toString());
		}
	}

	public synchronized short gets(String name) {
		try {
			return ((Short)get(name)).shortValue();
		} catch (ClassCastException ex) {
			try {
				return Short.parseShort(getTS(name));
			} catch (NumberFormatException e) { }

			throw new UnsupportedOperationException(
					"the field: " + name + " is a " +
					get(name).getClass() + " not a Short in " +
					"this message\n" + toString());
		}
	}

	public synchronized char getc(String name) {
		try {
			return ((Character)get(name)).charValue();
		} catch (ClassCastException ex) {
			if(getTS(name).length() == 1)
				return getTS(name).charAt(0);

			throw new UnsupportedOperationException(
					"the field: " + name + " is a " +
					get(name).getClass() + " not a Character in " +
					"this message\n" + toString());
		}
	}

	public synchronized int geti(String name, int def) {
		try { return geti(name); }
		catch (Exception ex) { return def; }
	}

	public synchronized short gets(String name, short def) {
		try { return gets(name); }
		catch (Exception ex) { return def; }
	}

	public synchronized char getc(String name, char def) {
		try { return getc(name); }
		catch (Exception ex) { return def; }
	}

	public synchronized long getl(String name, long def) {
		try { return getl(name); }
		catch (Exception ex) { return def; }
	}

	public synchronized float getf(String name, float def) {
		try { return getf(name); }
		catch (Exception ex) { return def; }
	}

	public synchronized double getd(String name, double def) {
		try { return getd(name); }
		catch (Exception ex) { return def; }
	}

	public synchronized byte getb(String name, byte def) {
		try { return getb(name); }
		catch (Exception ex) { return def; }
	}

	public synchronized boolean getp(String name, boolean def) {
		try { if(has(name)) return getp(name); else return def; }
		catch (Exception ex) { return def; }
	}

	public synchronized String getz(String name, String def) {
		try { return getz(name); }
		catch (Exception ex) { return def; }
	}

	public synchronized String getTS(String name, String def) {
		try { return getTS(name); }
		catch (Exception ex) { return def; }
	}

	public synchronized Object get(String name, Object def) {
		try { return get(name); }
		catch (Exception ex) { return def; }
	}

	public synchronized int geti(String name) {
		try {
			return ((Integer)get(name)).intValue();
		} catch (ClassCastException ex) {
			try {
				return Integer.parseInt(getTS(name));
			} catch (NumberFormatException e) { 
				try {
					return (int)Double.parseDouble(getTS(name));
				} catch(NumberFormatException ee) {
				}
			}

			throw new UnsupportedOperationException(
					"the field: " + name + " is a " +
					get(name).getClass() + " not a Integer in " +
					"this message\n" + toString());
		}
	}

	public synchronized long getl(String name) {
		try {
			return ((Long)get(name)).longValue();
		} catch (ClassCastException ex) {
			try {
				return Long.parseLong(getTS(name));
			} catch (NumberFormatException e) { }

			throw new UnsupportedOperationException(
					"the field: " + name + " is a " +
					get(name).getClass() + " not a Long in " +
					"this message\n" + toString());
		}
	}

	public synchronized float getf(String name) {
		try {
			return ((Float)get(name)).floatValue();
		} catch (ClassCastException ex) {
			try {
				return Float.parseFloat(getTS(name));
			} catch (NumberFormatException e) { }

			throw new UnsupportedOperationException(
					"the field: " + name + " is a " +
					get(name).getClass() + " not a Float in " +
					"this message\n" + toString());
		}
	}

	public synchronized double getd(String name) {
		try {
			return ((Double)get(name)).doubleValue();
		} catch (ClassCastException ex) {
			try {
				return Double.parseDouble(getTS(name));
			} catch (NumberFormatException e) { }

			throw new UnsupportedOperationException(
					"the field: " + name + " is a " +
					get(name).getClass() + " not a Double in " +
					"this message\n" + toString());
		}
	}

	private synchronized String toString(Object value) {
		if(value == null) return "true";
		else return value.toString();
	}

	public synchronized String getv(String name) {
		return toString(get(name));
	}

	private synchronized String toStringI() {
		StringBuffer sb = new StringBuffer();

		sb.append(check("#type")); sb.append("\n\t");
		for(Iterator it = map.entrySet().iterator(); it.hasNext(); ) {
			Map.Entry e = (Map.Entry)it.next();
			String key = (String)e.getKey();

			if(!key.startsWith("#")) {
				sb.append(
						key + "='" + toString(e.getValue()) + "'" + 
						(it.hasNext() ? "\n\t" : ""));
			}
		}

		return new String(sb);
	}

	public synchronized String toString() {
		return "<" + toStringI() + " />";
	}

	public synchronized String toStringXML_Open() {
		return "<" + toStringI() + ">";
	}

	public synchronized String toStringXML_Close() {
		return "</" + getz("#type") + ">";
	}

	public synchronized boolean has(String key) {
		int colon = key.indexOf(':');
		if(colon != -1) {
			String key_ = key.substring(0, colon);
			String value = key.substring(colon + 1);
			return has(key_) && getv(key_).equals(value);
		} else {
			return map.containsKey(key);
		}
	}

	public synchronized boolean has(String key, Object value) {
		Object v = get(key);
		if(v == null && value == null) return true;
		else if((v == null) != (value == null)) return false;
		else try {
			return v.equals(value);
		} catch (Exception ex) {
			return false;
		}
	}

	public synchronized String type() {
		return getz("#type");
	}

	public synchronized boolean isType(String type) {
		return Hierarchy.childOf(type(), type);
	}

	public synchronized Message adoptAs(String name, String target, Message src) {
		Object data = src.get(name);
		if(data != null) {
			set(target, data);
		}
		return this;
	}

	public synchronized Message adopt(Message src) {
		return adopt("*", src);
	}

	public synchronized Message adopt(String match, Message src) {
		if(src == null) 
			return this;

		if(match.indexOf("*") == -1) {
			if(src.has(match)) 
				set(match, src.get(match, null));
		}
		else for(Iterator it = src.map.entrySet().iterator();
				it.hasNext(); ) {
			Map.Entry entry = (Map.Entry)it.next();
			String key = (String)entry.getKey();
			if(!key.startsWith("#") && Hierarchy.childOf(key, match))
				set(key, entry.getValue());
				}
		return this;
	}

	/** Create a new message which only contains fields
	 * which match the pattern.
	 *
	 * @see leeps.utility.data.Hierarchy about matching.
	 */
	public synchronized Message select(String match) {
		return new Message(type()).adopt(match, this);
	}

	/** Create a new message with fields restricted to match the
	 *  selected patterns.
	 *
	 * @see leeps.utility.data.Hierarchy about matching.
	 */
	public synchronized Message select(String[] matches) {
		Message result = new Message(type());
		for(int i = 0; i < matches.length; i++) {
			result.adopt(matches[i], this);
		}
		return result;
	}

	/** Remove all elements which match the given pattern 
	 *
	 * @see leeps.utility.data.Hierarchy about matching.
	 */
	public synchronized Message clear(String match) {
		for(Iterator it = map.entrySet().iterator();
				it.hasNext(); ) {
			Map.Entry entry = (Map.Entry)it.next();
			String key = (String)entry.getKey();
			if(Hierarchy.childOf(key, match))
				it.remove();
				}
		return this;
	}

	/** Returns Map.Entry elements of the message */
	public synchronized Iterator iterator() {
		final Iterator it = map.entrySet().iterator();
		return new Iterator() {
			Map.Entry next;
			{ getNext(); }
			private void getNext() { 
				do {
					next = it.hasNext() ? (Map.Entry)it.next() : null; 
				} while(next != null 
						&& ((String)next.getKey())
						.startsWith("#"));
			}
			public boolean hasNext() { return next != null; }
			public Object next() {
				Map.Entry result = next;

				if(result == null)
					throw new java.util.NoSuchElementException();

				getNext();

				return result;
			}

			public void remove() {
				throw new UnsupportedOperationException(
						"remove() not supported");
			}
		};
	}

	/** Returns selected Map.Entry elements of the message */
	public synchronized Iterator iterator(String match) {
		return new Message(type()).select(match).iterator();
	}

	/** {@inheritDoc} */
	public synchronized Object clone() {
		return new Message(map);
	}

	/** Like clone, but returns a <tt>Message</tt> rather
	 * than an <tt>Object</tt>. 
	 */
	public synchronized Message copy() {
		return (Message)clone();
	}

	/**
	 * Compares two Messages.
	 *
	 * @return true if the maps consider themselves equal, false otherwise.
	 */
	public boolean equals(Object obj) {
		try {
			return obj instanceof Message 
				? map.equals(((Message)obj).map)
				: false;
		} catch(Exception ex) {
			return false;
		}
	}

	/** Provides a read-only view of the internal Map */
	public synchronized Map getMap() {
		return Collections.unmodifiableMap(map);
	}

	/** Get all fields with the prefix, w/out the
	 * the prefix.
	 */
	public synchronized Message extract(String prefix) {
		Message m = new Message(this.type());
		if(!prefix.endsWith(".")) prefix += ".";
		final int len = prefix.length();
		for(Iterator it = map.entrySet().iterator(); it.hasNext(); ) {
			Map.Entry entry = (Map.Entry)it.next();
			String key = (String)entry.getKey();
			if(key.startsWith(prefix))
				m.set(key.substring(prefix.length()), entry.getValue());
		}
		return m;
	}
}
